1 一个 Hello, World 程序的生命周期

本节内容是对整个课程内容的概述性介绍。

本节中可能会引入大量全新的概念,但请不要慌张——这些概念都会在后续章节中进行详细的阐述。现在只需要形成一个模糊的但能够纵览整个课程内容的认知就好。

本节内容需要随时在某个概念被详细介绍后添加指向对应介绍笔记的链接。

这是一个十分普通的,能够向标准输出流输出一行字符串 Hello, world! 的 C 程序:

#include <stdio.h>

int main()
{
	printf("Hello, world!\n");
	return 0;
}

让我们来纵览一下它从被创建到执行完成的一整个生命周期吧。

源文件

上面的代码本质上仍然只是我们手动写入到一个文件名为 hello.c 的文本文件中的字符串。它们在硬盘中以一系列值 0 与 1 的位(bit)序列的形式存放。8 个位为一个字节(byte)。现代计算机采用 UTF-8ASCII 标准,为每一个特定的 bit 序列赋予一个字符的意义。计算机根据读取到的 bit 序列,将其解释为对应的字符,并在屏幕上显示出来。

不仅仅是文本——计算机系统中所有的信息都是由一串串 bit 序列表示的。而读取这些 bit 时所处的 环境 决定了我们如何理解、区分这些 bit 序列。2 信息的表示与处理 会讲解字符、整数、小数等基本数据类型的存储与表示方式。

翻译

我们书写的 C 语言程序虽然能够被有编程基础的人类读懂,但机器却无法理解这些指令。因此,为了在机器上实际运行该程序,我们需要将这些高级语言代码经一些程序转化为一系列机器可以理解并执行的 低级机器指令。这些指令以 可执行目标程序 的格式打好包,存放在二进制磁盘文件中。

Unix 系统中,从源文件到目标程序的转化是由 编译系统 完成的。例如 gccclangmsvc 等 C 语言编译器。注意这里所述的“编译”是一个十分笼统的概念,实际上,其由 预处理、编译、汇编、链接 四个主要阶段组成。执行每个阶段的程序都不同,所有这些程序一同构成了一套复杂的编译工具链。我们以 gcc 为例讲解这四个阶段:

  1. 预处理阶段:预处理器(cpp)根据源程序中所有以 # 开头的命令(这被称作预处理命令)对源文件作修改。例如在上面的程序中,cpp 会根据 #include <stdio.h> 命令寻找到 stdio.h 这一系统库文件,并将其内容直接插入到该源文件中。预处理阶段得到一个以 .i 为扩展名的文本文件。
  2. 编译阶段:编译器(ccl)将文本文件 hello.i 翻译为包含一系列 汇编指令 的汇编语言程序 hello.s,该程序包含函数 main 的定义。
  3. 汇编阶段:汇编器(as)将 hello.s 翻译为机器语言指令,并打包为 可重定位目标程序(Relocatable Object Program) 的格式,将结果保存在二进制文件 hello.o 中,其包含了函数 main 的指令编码。
  4. 链接阶段:我们书写的程序调用了 C 标准库中的函数 printf,而它并不存在于 hello.o 而是存在一个单独的预编译目标文件 printf.o 中。因此,链接器(ld)负责将二者合并,并得到最终的文件名为 hello 的可执行文件。

运行前

在 Unix 系统中,我们需要一个名叫 shell 的应用程序来运行编译好的程序。shell 是一个命令行解释器,其等待用户输入一条命令,并执行输入的命令。在 GNU/Linux 中,我们将 shell 定位到 hello 程序所在的文件夹,并输入

./hello

即可运行该程序。

硬件系统

为了理解 hello 程序在被执行时究竟发生了什么,我们需要先对计算机的基本架构有一个基本认知。

这是一台典型的 PC 机的硬件组成:

Pasted image 20250422220104.png

其由如下几个主要部分组成:

运行

接下来我们来从一个非常宏观、粗略的视角来分析运行 hello 程序时究竟发生了什么:

起初我们处于 shell 程序中,它正等待我们输入一个命令。

我们从键盘上键入 ./hello 命令,CPU 将字符逐一读入寄存器,然后放入主存中。当我们按下回车键后,shell 程序知道我们结束了命令输入,随后其会执行一系列指令(从硬件角度来看这些都是 CPU 完成的)来加载存储在硬盘上的 hello 可执行文件:将该文件的代码与数据从磁盘复制到主存,执行代码。这些代码涉及了向标准输出流上输出字符串 Hello, world!,因此这些字符串先被复制到 CPU 的寄存器中,再从寄存器被复制到显示设备中,最终显示在屏幕上。如下图所示:

Pasted image 20250429142949.png

在过去,所有的数据存取操作都需要 CPU 完成,CPU 从磁盘中读取数据存入寄存器,随后 CPU 再将寄存器的内容存入主存中。由于当时 CPU 的运行频率远远高于其余部件,这样做可以加快数据读写。然而在今天,各种外围设备的运行频率也得到了极大提升,如果仍然让 CPU 处理所有的数据存取操作将极大拖慢 CPU 执行指令的效率。因此,直接存储器访问(Direct Memory Access,DMA) 技术应运而生,该技术允许数据从一个地址空间在无 CPU 干预的情况下直接复制到另外一个地址空间,使得外设与存储器、存储器与存储器之间直接的高速数据传输成为可能。如下图所示:

Pasted image 20250429143013.png

即使有着 DMA 技术的存在,从上面的例子中仍然可以看出程序执行时会将大量时间花费在把数据从一处移动到另一处。因此为了加快数据的读取速度,加快数据的传输过程必不可少。然而,存储器的”数据容量、存取速度、单位容量的造价“构成了一个不可能三角。寄存器文件一般只能缓存几字节至几百字节的数据,而当前计算机的主存容量以 GB 为单位衡量,磁盘的容量甚至可以达到几 TB 至几百 TB。此外,处理器与主存之间的读取速度差距也在增大:例如目前 DDR4 内存的时钟频率为 4800MHz,而 CPU 的时钟频率却能达到 3GHz。为了解决这种问题,计算机设计者采取了两种方法:

Pasted image 20250429143037.png

操作系统

在上面的例子中,从软件的角度来看,读取键盘输入指令,加载 hello 程序等操作似乎都是 shell 程序完成的。然而 shell 与 hello 程序都没有直接访问键盘、显示器、磁盘或是主存。这些程序通过调用 操作系统(Operating System,OS) 提供的 系统调用(System Call) 完成这一点。操作系统是介于计算机硬件与软件之间的一层特殊程序,其负责管理计算机的所有硬件资源供软件使用,所有应用程序都必须通过操作系统提供的系统接口才能实现对硬件资源的间接调用。操作系统通过三个核心抽象:

进程 是操作系统对一个正在运行的程序的抽象。在一个系统上可以同时运行多个进程,而每个进程 似乎 在运行时独占使用硬件。多个进程同时运行被叫做 并发 运行,其本质是多个进程的指令是交错在 CPU 上执行的,操作系统实现这种交错执行的机制被叫做 上下文切换。上下文指的是一个程序运行所需的所有状态信息,操作系统跟踪这些信息,在决定需要将硬件的控制权交换到另一个进程上时,它保存正在运行的程序的上下文,恢复新进程的上下文,然后将控制权传递给新进程。

从一个进程到另一个进程的转换由操作系统 内核(kernel) 管理。内核,即操作系统代码常驻主存的部分,内核不是一个独立的进程,而是操作系统管理全部进程所用代码和数据结构的集合。应用进程需要访问硬件而进行系统调用时,本质是将控制权转移给内核,由内核调用硬件完成相应指令并将结果和控制权返还给应用进程。例如我们一直举的例子中,就发生了如下图所示的上下文切换:

Pasted image 20250429150242.png

在现代的应用程序中,一个进程可以可以由多个 线程(Thread) 组成,所有线程运行在该进程的上下文中。由于现代的处理器一般是多核心的,多线程可以加快程序运行速度。第十二章会介绍并发与线程的详细知识。

虚拟内存 同样是操作系统提供给应用进程的抽象,它可以使得每个进程都认为自己 独占使用主存,而内存的每个字节都由一个唯一的数字来表示,这被称为该字节的地址(address),所有可能地址的集合被称作 虚拟地址空间(Virtual Address Space)。每个进程看到的内存都是一致的。在 Unix 系统中,每个进程的虚拟地址空间一般都由下图组成:

Pasted image 20250429151037.png

进程看到的虚拟地址空间由多个被准确定义的区组成,每个区都有着专门的功能,从低地址向高地址依次是:

第九章将介绍编译器与运行时系统如何将存储器空间划分为可管理的单元用于存放不同的 程序对象(Program Objects),即程序数据、指令与控制信息的集合。一种机制是 C 语言中的指针,其值为它指向的对象在内存中第一个字节的虚拟地址。C 编译器会维护指针的大小与类型信息,并根据指针的类型生成不同的机器代码,但其生成的可执行程序却不包含这些类型信息。

文件 本质上就是字节序列。在 Unix 系统中,所有输入输出都是通过使用一小组 Unix I/O 系统调用函数实现的。

从上面的粗略描述中,我们可以窥见:系统是硬件与系统软件交织的集合体,它们必须协作才能达到运行应用程序的最终目的。

其它重要主题

互联网

在当下,各种计算机设备通过互联网连接在一起,相互传输数据。从某个单独的设备来看,网络本质上也只是一个虚拟的 I/O 设备,可以发送或读取数据。第 11 章或 1.1 什么是互联网 会详细介绍计算机网络的相关知识。

Amdahl 定律

我们已经知道计算机是由大量的硬件与软件交织组成的系统。那么我们提升了该系统中某一个部分的性能,这对提升整个计算机系统的性能有多少帮助呢?

Amdahl 定律指出,当我们对系统的某个部分加速时,其对系统整体的加速程度取决于该部分的重要程度与加速程度。

具体的,设该系统原来执行某个任务耗时为 t,其中占比为 α 的时间用于执行某一特定的部分。现在我们对该部分做优化,现在运行该部分的用时为之前的 1k。则现在系统执行该任务的耗时为:

t=[(1α)+αk]t

假设我们现在拥有无限的资源可以使得 k。但整个系统的用时也只会变为原来的 1α 倍。这表明,为了显著加速整个系统,必须提升该系统中相当大一部分的运行速度。

并发与并行

并发(Concurrency) 是指同时具有多个活动的系统;而 并行(Parallelism) 则指使用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层级上运用: